<!DOCTYPE html>
<html style="height: 100%">

<head>
  <meta charset="utf-8">
  <script type="application/javascript" src="../js/jquery.js"></script>
  <script type="application/javascript" src="../js/jquery.mousewheel.min.js"></script>
  <script type="application/javascript" src="../js/plotly-locale-zh-cn-latest.js"></script>
  <script type="application/javascript" src="../js/plotly-latest.min.js"></script>
  <script type="application/javascript" src="../js/flatbuffers.js"></script>
  <script type="application/javascript" src="../js/ComponentInfo_generated.js"></script>
  <script type="application/javascript" src="../js/Contour_generated.js"></script>
  <script type="application/javascript" src="../js/DistanceSensorStatus_generated.js"></script>
  <script type="application/javascript" src="../js/FuserResponse_generated.js"></script>
  <script type="application/javascript" src="../js/LaserScan_generated.js"></script>
  <script type="application/javascript" src="../js/MechanicalComponent_generated.js"></script>
  <script type="application/javascript" src="../js/MotionType_generated.js"></script>
  <script type="application/javascript" src="../js/Obstacle_generated.js"></script>
  <script type="application/javascript" src="../js/ObstacleAlarm_generated.js"></script>
  <script type="application/javascript" src="../js/Position_generated.js"></script>
  <script type="application/javascript" src="../js/Vertex_generated.js"></script>
  <script type="application/javascript" src="dist/main.js"></script>
  <style type="text/css">
      @font-face {
          font-family: NotoSansSC;
          src: url('../fonts/NotoSansSC-Regular-Slim.otf') format('opentype');
      }

      body {
          font-family: NotoSansSC, Arial, sans-serif;
          height: 100%;
          margin: 0;
          display: flex;
          flex-direction: column;
      }

      tr {
          flex-grow: 1;
          display: flex;
          flex-direction: row;
      }

      .hide {
          display: none !important;
      }

      .tooltip {
          position: relative;
          display: inline-block;
      }

      .tooltip .tooltiptext {
          visibility: hidden;
          padding: 0px 5px 0px 5px !important;
          /* width: 120px; */
          background-color: #555;
          color: #fff;
          text-align: center;
          border-radius: 6px;
          padding: 5px 0;
          position: absolute;
          z-index: 1;
          bottom: 125%;
          left: 20px;
          /* margin-left: -60px; */
          opacity: 0;
          transition: opacity 0.3s;
      }

      .tooltip .tooltiptext::after {
          content: "";
          position: absolute;
          top: 100%;
          left: 20px;
          margin-left: -5px;
          border-width: 5px;
          border-style: solid;
          border-color: #555 transparent transparent transparent;
      }

      .tooltip:hover .tooltiptext {
          visibility: visible;
          opacity: 1;
      }

      .text {
          color: #CCCCCC;
      }

      .nowrap {
          white-space: nowrap;
      }

      .box {
          border: 1px solid #555;
      }

      .loading_info {
          height: 50px;
          display: flex;
          flex-direction: row;
          padding-left: 100px;
      }

      .loading_info>svg {
          width: 50px;
          height: 50px;
          animation: rotate 1s linear infinite;
      }

      @keyframes rotate {
          0% {
              transform: rotate(0deg);
          }

          100% {
              transform: rotate(360deg);
          }
      }

      .loading_info>span {
          display: block;
          width: 200px;
          height: 50px;
          line-height: 50px;
          margin-left: 20px;
      }

      #frame {
          flex-grow: 1;
          display: flex;
          flex-direction: row;
      }

      #left {
          position: relative;
          height: 100%;
          width: 350px;
          display: flex;
          flex-direction: column;
      }

      #checked_items {
          display: flex;
          flex-wrap: wrap;
      }

      #radar_detail_parent2 {
          position: relative;
          width: 100%;
          height: 100%;
          overflow: hidden;
      }

      #radar_detail_parent {
          position: absolute;
          width: 100%;
          height: 100%;
      }

      #radar_detail_parent_ext {
          text-align: center;
      }

      #radar_detail_content {
          width: 100%;
          margin-top: -50px;
      }

      table {
          width: 100%;
          text-align: center;
          border-spacing: 0px;
      }

      tbody {
          width: 100%;
      }

      tr {
          width: 100%;
          height: 25px;
      }

      td {
          display: inline-block;
          height: 25px;
          padding: 0px;
          line-height: 25px;
      }

      .td0 {
          width: 65%;
          white-space: nowrap;
      }

      .td1 {
          width: 35%;
          white-space: nowrap;
      }

      #right {
          position: relative;
          flex-grow: 1;
          display: flex;
          flex-direction: row;
      }

      #graph_div {
          width: 80%;
          height: 100%;
      }

      #graph_div_ext {
          height: 100%;
          flex-grow: 1;
      }

      #txt_choice {
          padding-left: 20px;
      }

      #status_bar {
          padding-left: 20px;
      }

      #popup_menu span {
          display: block;
          padding: 5px 5px 5px 5px;
          cursor: pointer;
      }

      #popup_menu span:hover {
          color: blue;
      }
  </style>
</head>

<body>
<div>
  <input id="filtertime_btn" type="button" value="filter time" />
  <input id="datetimepicker" type="text" autocomplete="off" class="box"
         style="display: inline-block; width: 160px;" />
  <input id="datetimepicker_btn" type="button" style="display: inline-block;" value="jump" />
</div>
<!-- <div id="collapse_btn">&gt;&gt;</div> -->
<div id="frame">
  <div id="left">
    <p id="checked_items">&emsp;</p>
    <table>
      <thead>
      <tr>
        <td id="txt_timestamp" class="td0">timestamp</td>
        <td id="txt_radar_id" class="td1">radar id</td>
      </tr>
      </thead>
    </table>
    <div id="radar_detail_parent2">
      <div id="radar_detail_parent">
        <div id="radar_detail_content">
          <div id="pre_load" class="loading_info">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"
                 preserveAspectRatio="xMidYMid">
              <g data-idx="1">
                <circle stroke-dasharray="164.93 56.98" r="35" stroke-width="10" stroke="#faca30"
                        fill="none" cy="50" cx="50" data-idx="2"
                        transform="matrix(-1.00,-0.06,0.06,-1.00,96.76,103.04)">
                </circle>
              </g>
            </svg>
            <span>loading...</span>
          </div>
          <div id="pre_load_no" class="loading_info hide">
            <span>no more data</span>
          </div>
          <table id="radar_detail">
            <tbody></tbody>
          </table>
          <div id="post_load" class="loading_info">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"
                 preserveAspectRatio="xMidYMid">
              <g data-idx="1">
                <circle stroke-dasharray="164.93 56.98" r="35" stroke-width="10" stroke="#faca30"
                        fill="none" cy="50" cx="50" data-idx="2"
                        transform="matrix(-1.00,-0.06,0.06,-1.00,96.76,103.04)">
                </circle>
              </g>
            </svg>
            <span>loading...</span>
          </div>
        </div>
      </div>
      <p id="radar_detail_parent_ext" class="hide"><span>no data</span></p>
    </div>
  </div>
  <div id="right">
    <div id="graph_div"></div>
    <div id="graph_div_ext">
    </div>
  </div>
</div>
<p id="status_bar">current motion: ready</p>
<div id="shadow_view"
     style="position: absolute; display: none; left: 0px; top: 0px; right: 0px; bottom: 0px; z-index: 1001; background-color: #9997;">
  <div id="popup_menu"
       style="position: absolute; width: 200px; background-color: #EEE; border: 1px solid #333; border-radius: 5px; left: 0px; top: 0px;">
    <div id="menu_export_2dpoint"><span>export 2dpoint frame</span></div>
    <div id="menu_export_3dpoint"><span>export 3dpoint frame</span></div>
    <div id="menu_view_rgbimage"><span>view rgb image</span></div>
    <div id="menu_view_depthimage"><span>view depth image</span></div>
    <div id="menu_view_infraimage"><span>view infrared image</span></div>
  </div>
  <div id="image_view"
       style="position: absolute; display: flex; left: 0px; top: 0px; right: 0px; bottom: 0px; justify-content: center; align-items: center;">
    <img id="image_view_ctx" />
  </div>
</div>
<script type="text/javascript">
  Date.prototype.format = function (format) {
    return format
      .replace('yyyy', this.getFullYear())
      .replace('yy', `0${this.getFullYear()}`.slice(-2))
      .replace('MM', `0${this.getMonth() + 1}`.slice(-2))
      .replace('M', (this.getMonth() + 1))
      .replace('dd', `0${this.getDate()}`.slice(-2))
      .replace('d', (this.getDate()))
      .replace('HH', `0${this.getHours()}`.slice(-2))
      .replace('H', this.getHours())
      .replace('mm', `0${this.getMinutes()}`.slice(-2))
      .replace('m', this.getMinutes())
      .replace('ss', `0${this.getSeconds()}`.slice(-2))
      .replace('s', this.getSeconds())
      .replace('SSS', `00${this.getMilliseconds()}`.slice(-3));
  };
  Array.prototype.sortedInsert = function (num, dupIgnore) {
    let start = 0;
    let end = this.length - 1;

    while (start <= end) {
      let middle = Math.floor((start + end) / 2);

      if (num === this[middle]) {
        if (!dupIgnore)
          this.splice(middle, 0, num);
        return;
      } else if (num < this[middle]) {
        end = middle - 1;
      } else {
        start = middle + 1;
      }
    }
    this.splice(start, 0, num);
  }
  var handle_icon_ = "path://M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4v1.3h1.3v-1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7V23h6.6V24.4z M13.3,19.6H6.7v-1.4h6.6V19.6z";
  function fmt2(num) {
    return ('0' + num).slice(-2);
  }
  function format_bytes(bytes) {
    if (bytes < 1024) {
      return `${bytes} B`;
    } else if (bytes < 1024 * 1024) {
      return `${(bytes / 1024).toFixed(2)} KB`;
    } else if (bytes < 1024 * 1024 * 1024) {
      return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
    } else if (bytes < 1024 * 1024 * 1024 * 1024) {
      return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
    } else {
      return `${(bytes / (1024 * 1024 * 1024 * 1024)).toFixed(2)} TB`;
    }
  }

  class Position {
    constructor(x, y, theta) {
      this.x = x;
      this.y = y;
      this.theta = theta;
    }
  }

  class ComponentNode {
    constructor(name, model) {
      this.name = name;
      this.model = model;
      this.absPosition = null;
      this.children = [];
    }
  }

  function try_parse_time(time) {
    if (time.length == 10) {
      return new Date(`${time} 12:00:00`).getTime();
    } else if (time.length == 13) {
      return new Date(`${time}:30:00`).getTime();
    } else if (time.length == 16) {
      return new Date(`${time}:30`).getTime();
    } else if (time.length == 19 || time.length == 23) {
      return new Date(time).getTime();
    } else {
      return null;
    }
  }

  var call_flutter_ = [];
  function invoke_flutter(func, args) {
    call_flutter_.push({ func: func, args: args });
  }

  function flutter_filter_time() {
    invoke_flutter('filter_time', []);
  }

  function process_sketchy_items(sketchy_items_raw) {
    if (sketchy_items_raw.length > 0) {
      if (sketchy_items_raw[0].radarId == undefined) {
        for (var i = 0; i < sketchy_items_raw.length; i += 1) {
          sketchy_items_raw[i].radarId = sketchy_items_raw[i].nodeDeviceId;
          sketchy_items_raw[i].steadyClockMs = sketchy_items_raw[i].nodeSteadyClockMs;
          delete sketchy_items_raw[i].nodeDeviceId;
          delete sketchy_items_raw[i].nodeSteadyClockMs;
          delete sketchy_items_raw[i].nodeIp;
        }
      }
    }
  }

  var SketchyUpdateMode = { Current: 0, Before: 1, After: 2, BeforePage: 3, AfterPage: 4 };
  var TimestampType = { Start: 0, Middle: 1, End: 2 };
  var PlayStatus = { Stopped: 0, WaitingForPlay: 1, Playing: 2 };

  var DateTimePicker = {
    currentMicros: null,

    init: function () {
      $('#filtertime_btn').click(flutter_filter_time);
      $('#datetimepicker_btn').click(async function () {
        DateTimePicker.currentMicros = DateTimePicker.getTimeMicros();
        invoke_flutter('selected_time', [DateTimePicker.getTimeMicros()]);
        await DateTimePicker.updateSketchyByCurrentTime();
      });
      //
      var queryParams = new URLSearchParams(window.location.search);
      var timePoint = queryParams.get('timePoint');
      if (timePoint) {
        timePoint = new Date(parseInt(timePoint) / 1000);
      } else {
        timePoint = new Date(new Date().getTime() - 5000);
      }
      this.setTime(timePoint);
    },

    updateSketchyByCurrentTime: async function () {
      SketchyList.cacheItems = [];
      await SketchyList.updateItemsImpl(this.currentMicros, TimestampType.Middle, 100);
      SketchyList.adjustSelectItemPosToCenter();
    },

    getTimeMillis: function () {
      var ts_millis = try_parse_time($('#datetimepicker').val());
      if (ts_millis != null) return ts_millis;
      var time = new Date();
      this.setTime(time);
      return ts_millis.getTime();
    },

    getTimeMicros: function () {
      return this.getTimeMillis() * 1000;
    },

    setTime: function (time) {
      this.currentMicros = time.getTime() * 1000;
      $('#datetimepicker').val(time.format("yyyy-MM-dd HH:mm:ss.SSS"));
    },

    setTimeByMicrosTs: function (tsMicros) {
      this.currentMicros = tsMicros;
      var time = new Date(Math.round(tsMicros / 1000));
      $('#datetimepicker').val(time.format("yyyy-MM-dd HH:mm:ss.SSS"));
    },

    setTimeByMicrosTsAndUpdate: async function (tsMicros) {
      this.setTimeByMicrosTs(tsMicros);
      await this.updateSketchyByCurrentTime();
    },
  };

  var RadarSelector = {
    allRadars: {},
    uncheckedItems: [],

    init: async function () {
      var radarItems = (await window.grpc.getAllRadarDataItems()).toObject().itemsMap;
      this.allRadars = {};
      for (var i = 0; i < radarItems.length; i += 1) {
        var item = radarItems[i];
        this.allRadars[item[0]] = item[1];
      }
      console.log(this.allRadars);
      this.refreshCheckboxes();
    },

    refreshCheckboxes: function () {
      $('#checked_items').html('');
      for (var radar_id in this.allRadars) {
        var radar_item = this.allRadars[radar_id];
        $('#checked_items').append(`<span class="nowrap"><input id="rd${radar_id}" class="detail_filter_rd" type="checkbox" name="radio_check" checked="checked" /><label for="rd${radar_id}" class="text">${radar_item.name}</label></span>`);
      }
      $('.detail_filter_rd').click(async function () {
        var radarId = parseInt($(this).attr('id').substr(2));
        if ($(this).is(':checked')) {
          RadarSelector.uncheckedItems = RadarSelector.uncheckedItems.filter(item => item != radarId);
        } else {
          RadarSelector.uncheckedItems.sortedInsert(radarId, true);
        }
        await SketchyList.refreshByMode(SketchyUpdateMode.Current);
        SketchyList.adjustSelectItemPosToCenter();
      });
    },

    getCurrentSelectedItems: function () {
      var dest_ts_micros = DateTimePicker.getTimeMicros();
      var ready_selected_items = {};
      for (var i = 0; i < SketchyList.cacheItems.length; i++) {
        var ts_micros = SketchyList.cacheItems[i].timestampMicros;
        var radar_id = SketchyList.cacheItems[i].radarId;
        var timespan_micros = Math.abs(dest_ts_micros - ts_micros);
        if (SketchyList.playStatus != PlayStatus.Playing && timespan_micros >= 100000) continue;
        if (RadarSelector.allRadars[radar_id] != null) {
          if (RadarSelector.uncheckedItems.indexOf(radar_id) >= 0) {
            continue;
          }
        } else {
          RadarSelector.allRadars[radar_id] = { name: `${i18n_data_["lidarId"]}-${radar_id}`, description: '', modelName: '~' };
          RadarSelector.refreshCheckboxes();
        }
        if (ready_selected_items[radar_id] == null || ready_selected_items[radar_id] > timespan_micros) {
          ready_selected_items[radar_id] = timespan_micros;
        }
      }
      var selected_items = new Set();
      for (item in ready_selected_items) {
        selected_items.add(parseInt(item));
      }
      return selected_items;
    },
  };

  var SketchyList = {
    marginTop: -50,
    isUpdating: false,
    cacheItems: [],
    playStatus: PlayStatus.Stopped,
    playStatusWaitElapse: new Date().getTime(),
    stopPlayCallback: function () { },

    setPreLoadStatus: function (no_more_data) {
      if (no_more_data) {
        $('#pre_load').addClass('hide');
        $('#pre_load_no').removeClass('hide');
      } else {
        $('#pre_load').removeClass('hide');
        $('#pre_load_no').addClass('hide');
      }
    },

    init: function () {
      $('#radar_detail_parent').on('mousewheel', function (e) {
        e.preventDefault();
        if (e.originalEvent) e = e.originalEvent;
        SketchyList.setMarginTop(SketchyList.marginTop - e.deltaY);
      });

      $('body').keydown(async function (e) {
        if (e.keyCode == 38) { // e.key == "ArrowUp"
          e.preventDefault();
          await SketchyList.refreshByMode(SketchyUpdateMode.Before);
        } else if (e.keyCode == 40) { // e.key == "ArrowDown"
          e.preventDefault();
          if (SketchyList.playStatus == PlayStatus.Stopped) {
            await SketchyList.refreshByMode(SketchyUpdateMode.After);
            SketchyList.playStatus = PlayStatus.WaitingForPlay;
            SketchyList.playStatusWaitElapse = new Date().getTime() + 1000;
          } else if (SketchyList.playStatus == PlayStatus.WaitingForPlay) {
            if (new Date().getTime() < SketchyList.playStatusWaitElapse) return;
            SketchyList.playStatus = PlayStatus.Playing;
            SketchyList.stopPlayCallback();
            SketchyList.stopPlayCallback = function () { };
            callback = window.grpc.startReplayRadarData(DateTimePicker.getTimeMicros(), async function (data) {
              console.log('on callback');
              if (SketchyList.playStatus != PlayStatus.Playing) return;
              await RadarPointView.todoPaint(data);
            });
            SketchyList.stopPlayCallback = function () {
              console.log('call stop callback');
              callback();
            };
          } else {
            return;
          }
        } else if (e.keyCode == 33) { // e.key == "PageUp"
          e.preventDefault();
          await SketchyList.refreshByMode(SketchyUpdateMode.BeforePage);
        } else if (e.keyCode == 34) { // e.key == "PageDown"
          e.preventDefault();
          await SketchyList.refreshByMode(SketchyUpdateMode.AfterPage);
        }
      });

      $('body').keyup(async function (e) {
        if (e.keyCode == 40) { // e.key == "ArrowDown"
          e.preventDefault();
          if (SketchyList.playStatus == PlayStatus.Playing) {
            SketchyList.stopPlayCallback();
            SketchyList.stopPlayCallback = function () { };
          }
          SketchyList.playStatus = PlayStatus.Stopped;
        }
      });
    },

    getNearFrameMicros: function (update_mode) {
      var near_frame_micros = {};
      for (var i = 0; i < this.cacheItems.length; i++) {
        var radar_id = this.cacheItems[i].radarId;
        if (RadarSelector.uncheckedItems.indexOf(radar_id) != -1) continue;
        var cur_span_micros = Math.abs(this.cacheItems[i].timestampMicros - DateTimePicker.currentMicros);
        if (near_frame_micros[radar_id] == null || near_frame_micros[radar_id].span_micros >= cur_span_micros) {
          near_frame_micros[radar_id] = {
            frame_id: this.cacheItems[i].timestampMicros,
            index: i,
            span_micros: cur_span_micros
          };
        }
      }
      if (Object.keys(near_frame_micros).length == 0) return null;
      if (update_mode == SketchyUpdateMode.Before || update_mode == SketchyUpdateMode.After) {
        var sel_indexs = [];
        for (var radar_id in near_frame_micros) {
          sel_indexs.push(near_frame_micros[radar_id].index);
        }
        //sel_indexs.sort();
        var new_index = 0;
        if (update_mode == SketchyUpdateMode.After) {
          new_index = Math.max(...sel_indexs) + 1;
        } else {
          new_index = Math.min(...sel_indexs) - 1;
        }
        if (new_index < 0 || new_index >= this.cacheItems.length) return null;
        DateTimePicker.setTimeByMicrosTs(this.cacheItems[new_index].timestampMicros);
        this.adjustSelectItemPosToCenter();
        return this.getNearFrameMicros(SketchyUpdateMode.Current);
      } else if (update_mode == SketchyUpdateMode.BeforePage || update_mode == SketchyUpdateMode.AfterPage) {
        var count_per_page = Math.round($('#radar_detail_parent').height() / 30);
        var direct_before = update_mode == SketchyUpdateMode.BeforePage;
        var new_index = null;
        for (var radar_id in near_frame_micros) {
          var tmp_index = near_frame_micros[radar_id].index;
          if (new_index == null) {
            new_index = tmp_index;
          } else if (direct_before) {
            new_index = Math.min(new_index, tmp_index);
          } else {
            new_index = Math.max(new_index, tmp_index);
          }
        }
        var inc_complete_any = false;
        while (count_per_page > 0) {
          if (new_index < 0 || new_index >= this.cacheItems.length) break;
          var inc_complete = false;
          if (direct_before) {
            while (new_index > 0) {
              new_index -= 1;
              if (!RadarSelector.uncheckedItems.indexOf(this.cacheItems[new_index].radarId) >= 0) {
                inc_complete = true;
                break;
              }
            }
          } else {
            while (new_index < this.cacheItems.length - 1) {
              new_index += 1;
              if (!RadarSelector.uncheckedItems.indexOf(this.cacheItems[new_index].radarId) >= 0) {
                inc_complete = true;
                break;
              }
            }
          }
          if (!inc_complete) break;
          inc_complete_any = true;
          count_per_page -= 1;
        }
        if (inc_complete_any) {
          if (new_index < 0 || new_index >= this.cacheItems.length) return null;
          DateTimePicker.setTimeByMicrosTs(this.cacheItems[new_index].timestampMicros);
          this.adjustSelectItemPosToCenter();
          return this.getNearFrameMicros(SketchyUpdateMode.Current);
        }
      }
      return DateTimePicker.getTimeMicros();
    },

    setMarginTop: function (new_margin_top) {
      this.marginTop = new_margin_top;
      var need_load_before = false, need_load_after = false;
      if (this.marginTop > -50) {
        need_load_before = true;
        if (this.marginTop > 0) this.marginTop = 0;
      }
      var view_height = $('#radar_detail_parent').height();
      var content_height = $('#radar_detail_content').height();
      if (this.marginTop + content_height < view_height + 50) {
        need_load_after = true;
        if (this.marginTop < view_height - content_height) this.marginTop = view_height - content_height;
      }
      $('#radar_detail_content').css('margin-top', `${this.marginTop}px`);
      if ((need_load_before || need_load_after) && this.playStatus != PlayStatus.Playing)
        this.updateItems(need_load_before, need_load_after);
    },

    adjustSelectItemPosToCenter: function () {
      if ($('#radar_detail_parent').hasClass('hide')) return;
      if (this.cacheItems.length == 0) return;
      if (this.playStatus == PlayStatus.Playing) {
        var outer_height = $('#radar_detail_parent').height();
        var inner_height = $('#radar_detail_content').height();
        this.marginTop = (outer_height - inner_height) / 2;
        $('#radar_detail_content').css('margin-top', `${this.marginTop}px`);
        return;
      }
      var sel_index = 0, dest_span_micros = Math.abs(this.cacheItems[0].timestampMicros - DateTimePicker.currentMicros);
      for (var i = 1; i < this.cacheItems.length; i++) {
        var radar_id = this.cacheItems[i].radarId;
        if (RadarSelector.uncheckedItems.indexOf(radar_id) != -1) continue;
        var cur_span_micros = Math.abs(this.cacheItems[i].timestampMicros - DateTimePicker.currentMicros);
        if (cur_span_micros < dest_span_micros) {
          sel_index = i;
          dest_span_micros = cur_span_micros;
        }
      }
      var dest_offset = $('#radar_detail_parent').height() * 0.65;
      var cur_offset = $(`#sk${this.cacheItems[sel_index].timestampMicros}`).offset()?.top ?? 0;
      this.setMarginTop(this.marginTop - cur_offset + dest_offset);
    },

    refreshByMode: async function (update_mode) {
      var selected_micros = this.getNearFrameMicros(update_mode);
      var selected_microses = await RadarPointView.doPaintByMicros(selected_micros);
      if (selected_microses == null) return;
      this.refreshBySelectedMicros(selected_microses);
    },

    refreshBySelectedMicros: function (selected_microses) {
      var content = '';
      for (var i = 0; i < this.cacheItems.length; i++) {
        var radar_id = this.cacheItems[i].radarId;
        if (RadarSelector.uncheckedItems.indexOf(radar_id) != -1) continue;
        var ts_micros = this.cacheItems[i].timestampMicros;
        var ts_millis = Math.round(ts_micros / 1000);
        var tsTimeStr = new Date(ts_millis).format("yyyy-MM-dd HH:mm:ss.SSS");
        var steadyMillis = this.cacheItems[i].steadyClockMs;
        var validTypes = JSON.stringify(this.cacheItems[i].validTypesList);
        var bg = selected_microses.has(ts_micros) ? 'style="background-color: #DDDDDD;"' : '';
        var radar_item = RadarSelector.allRadars[radar_id];
        if (radar_item == null) {
          console.log(`unknown radar_id[${radar_id}]. allRadars:`);
          console.log(RadarSelector.allRadars);
          return;
        }
        content += `<tr id="sk${ts_micros}" ${bg} class="radar_frame_item" data-radar-id="${radar_id}" data-steady-ms="${steadyMillis}" data-valid-types="${validTypes}"><td class="tooltip td0">${tsTimeStr}<span class="tooltiptext">steady_clock: ${steadyMillis}</span></td><td class="tooltip td1">${radar_item.name}<span class="tooltiptext">radar_id: ${radar_id}</span></td></tr>`;
      }
      if (content == '') {
        $('#radar_detail_parent').addClass('hide');
        $('#radar_detail_parent_ext').removeClass('hide');
      } else {
        $('#radar_detail_parent').removeClass('hide');
        $('#radar_detail_parent_ext').addClass('hide');
      }
      $('#radar_detail tbody').html(content);
      $('#radar_detail_content').css('margin-top', `${this.marginTop}px`);
      //var real_first_line_offset = $(`#sk${this.cacheItems[0].timestampMicros}`).offset().top;
      $('.radar_frame_item').click(async function () {
        DateTimePicker.setTimeByMicrosTs(parseInt($(this).attr('id').substr(2)));
        invoke_flutter('selected_time', [DateTimePicker.getTimeMicros()]);
        await SketchyList.refreshByMode(SketchyUpdateMode.Current);
      });
      $('.radar_frame_item').on("contextmenu", function (e) {
        e.preventDefault();
        var radarId = +$(this).attr('data-radar-id');
        var frameMicros = +$(this).attr('id').substr(2);
        var steadyMillis = +$(this).attr('data-steady-ms');
        var validTypes = JSON.parse($(this).attr('data-valid-types'));
        return ExporterUtils.onContextMenu(e, radarId, frameMicros, steadyMillis, validTypes);
      });
    },

    updateItemsImpl: async function (ts_micros, ts_type, count) {
      var sketchy_items_raw = await window.grpc.queryRadarDataSketchys(ts_micros, ts_type, count);
      var sketchy_items_raw = sketchy_items_raw.toObject().itemsList;
      process_sketchy_items(sketchy_items_raw);
      this.updateItemsRawImpl(sketchy_items_raw, ts_type);
      await this.refreshByMode(SketchyUpdateMode.Current);
    },

    updateItemsRawImpl: function (sketchy_items_raw, ts_type) {
      if (sketchy_items_raw.length == 0) {
        if (ts_type == TimestampType.End) {
          this.setPreLoadStatus(true);
        }
        return;
      }
      var move_count = 0;
      if (ts_type == TimestampType.Start) {
        this.cacheItems = this.cacheItems.concat(sketchy_items_raw);
        if (this.cacheItems.length > 300) {
          this.setPreLoadStatus(false);
          move_count = -this.filteredCacheItemCount(0, this.cacheItems.length - 300);
          this.cacheItems = this.cacheItems.slice(-300);
        }
      } else if (ts_type == TimestampType.End) {
        this.cacheItems = sketchy_items_raw.concat(this.cacheItems);
        move_count = this.filteredCacheItemCount(0, sketchy_items_raw.length);
        if (this.cacheItems.length > 300) {
          this.cacheItems = this.cacheItems.slice(0, 300);
        }
      } else {
        this.setPreLoadStatus(false);
        this.cacheItems = sketchy_items_raw;
      }
      for (var i = 0; i < this.cacheItems.length - 1; i += 1) {
        if (this.cacheItems[i].timestampMicros >= this.cacheItems[i + 1].timestampMicros) {
          this.cacheItems.splice(i + 1, 1);
          i -= 1;
        }
      }
      this.cacheItems = this.cacheItems.filter((item, index) => this.cacheItems.indexOf(item) === index);
      this.marginTop = this.marginTop - move_count * 25;
    },

    updateItems: async function (update_before, update_after) {
      if (!update_before && !update_after) return;
      if (this.isUpdating) return;
      this.isUpdating = true;
      //
      var dest_ts_micros = DateTimePicker.getTimeMicros();
      var ts_micros = -1, ts_type = null, count = 100;
      if (update_before && update_after) {
        ts_micros = dest_ts_micros;
        ts_type = TimestampType.Middle;
      } else if (update_before) {
        ts_micros = this.cacheItems.length > 0 ? this.cacheItems[0].timestampMicros : dest_ts_micros;
        ts_type = TimestampType.End;
      } else if (update_after) {
        ts_micros = this.cacheItems.length > 0 ? this.cacheItems[this.cacheItems.length - 1].timestampMicros : dest_ts_micros + 1;
        ts_type = TimestampType.Start;
      }
      try {
        await this.updateItemsImpl(ts_micros, ts_type, count);
        this.isUpdating = false;
      } catch (e) {
        this.isUpdating = false;
        throw e;
      }
    },

    filteredCacheItemCount: function (start, end) {
      var count = 0;
      for (var i = start; i < end; i += 1) {
        if (RadarSelector.uncheckedItems.indexOf(this.cacheItems[i].radarId) < 0) count += 1;
      }
      return count;
    }
  };

  var kComponentColors = {};
  kComponentColors[layout.ComponentType.kMovingBase] = 'red';
  kComponentColors[layout.ComponentType.kCollisionArea] = 'black';
  kComponentColors[layout.ComponentType.kCarFrame] = 'grey';
  kComponentColors[layout.ComponentType.kFixture] = 'blue';
  kComponentColors[layout.ComponentType.kGoods] = 'orange';
  kComponentColors[layout.ComponentType.kRack] = 'pink';
  kComponentColors[layout.ComponentType.kRackLeg] = 'grey';
  kComponentColors[layout.ComponentType.kRadar] = '#A93226';
  kComponentColors[layout.ComponentType.kSafetyEdge] = 'red';
  kComponentColors[layout.ComponentType.kForkFrame] = 'black';
  kComponentColors[layout.ComponentType.kForkMovementMechanism] = 'black';
  kComponentColors[layout.ComponentType.kDistanceSensor] = '#6699ff';

  var RadarPointView = {
    viewWidth: 100,
    viewElem: null,
    rootComponents: null,
    components: {},
    todoPaintDataRaw: null,
    lastUpdateMillis: 0,

    _getComponentColorByType: function (cpnType) {
      if (cpnType in kComponentColors) {
        return kComponentColors[cpnType];
      }
      return '#AAAAAA';
    },

    _rotatePoint: function (x, y, radians) {
      var newX = x * Math.cos(radians) - y * Math.sin(radians);
      var newY = x * Math.sin(radians) + y * Math.cos(radians);
      return { x: newX, y: newY };
    },

    _getObstacleColor: function (ob_type) {
      switch (ob_type) {
        case fuser.ObstaclePositionType.kDeadAhead: return 'red';
        case fuser.ObstaclePositionType.kLeftEdge: return 'Magenta';
        case fuser.ObstaclePositionType.kRightEdge: return 'Magenta';
        case fuser.ObstaclePositionType.kLeftClose: return 'violet';
        case fuser.ObstaclePositionType.kRightClose: return 'violet';
        case fuser.ObstaclePositionType.kLeftNearby: return 'pink';
        case fuser.ObstaclePositionType.kRightNearby: return 'pink';
        default: return 'green';
      }
    },

    init: async function () {
      this.viewWidth = $('#graph_div').height();
      $('#graph_div').width(this.viewWidth);
      this.viewElem = $('#graph_div')[0];
      var data = [];
      var layout = {
        margin: { r: 240 },
        xaxis: { range: [-6000, 6000], },
        yaxis: { range: [-6000, 6000], scaleanchor: 'x', scaleratio: 1, },
        height: this.viewWidth,
        width: this.viewWidth + 240,
        dragmode: 'pan',
        hovermode: 'closest',
        legend: {
          orientation: 'v', xanchor: 'center', x: 1.1, y: 1.0,
        },
      };
      var config = {
        displayModeBar: true,
        scrollZoom: true,
        displaylogo: false,
        modeBarButtonsToAdd: [
          'hoverClosest',
          'hoverCompare',
          'toggleSpikelines',
        ],
        modeBarButtonsToRemove: [
          'select2d',
          'lasso2d',
          // 'resetScale2d',
          'toggleSpikelines',
          // 'hoverCompareCartesian',
        ],
      };
      Plotly.newPlot(this.viewElem, data, layout, config);
      //
      var components_raw = (await window.grpc.getComponentInfos()).getComponentDatas_asU8();
      this.doPaintContoursImpl(components_raw);
    },

    _set_abs_position: function (cur_comp_obj) {
      if (cur_comp_obj == null) return;
      var fbPos = cur_comp_obj.model.relPosition();
      var pos = new Position(fbPos.x(), fbPos.y(), fbPos.theta());
      var par_comp_obj = this.components[cur_comp_obj.model.baseName()];
      while (par_comp_obj != this.rootComponents && par_comp_obj != null) {
        var origX = pos.x;
        var origY = pos.y;
        var par_rel_pos = par_comp_obj.model.relPosition();
        var par_theta = par_rel_pos.theta();
        pos.x = par_rel_pos.x() + origX * Math.cos(par_theta) - origY * Math.sin(par_theta);
        pos.y = par_rel_pos.y() + origX * Math.sin(par_theta) + origY * Math.cos(par_theta);
        pos.theta = pos.theta + par_theta;
        var base_name = par_comp_obj.model.baseName();
        if (base_name == par_comp_obj.name) break;
        par_comp_obj = this.components[base_name];
      }
      cur_comp_obj.absPosition = pos;
    },

    _getComponentShapes: function () {
      var _component_iterator = function* (cur_obj) {
        yield cur_obj;
        for (var i = 0; i < cur_obj.children.length; i += 1) {
          var child_iter = _component_iterator(cur_obj.children[i]);
          while (true) {
            var value = child_iter.next();
            if (value.done) break;
            yield value.value;
          }
        }
      };
      if (this.rootComponents == null) return [];
      var iter = _component_iterator(this.rootComponents);
      var shapes = [];
      while (true) {
        var iter_item = iter.next();
        if (iter_item.done) break;
        var cur_comp_obj = iter_item.value;
        var comp_name = cur_comp_obj.model.uniqueName();
        var parent_name = cur_comp_obj.model.baseName();
        var posture = cur_comp_obj.model.posture();
        var path = '';
        for (var j = 0; j < posture.verticesLength(); ++j) {
          var vertex = posture.vertices(j);
          path += `${j == 0 ? 'M ' : ' L'}${vertex.x()},${vertex.y()}`;
        }// 2024-05-27 10:00:35.516
        path += ' Z';
        var opacity = 0.5;
        var category = cur_comp_obj.model.category();
        if (category == layout.ComponentCategory.kVirtual) {
          opacity = 0.1;
        } else if (category == layout.ComponentCategory.kSensor) {
          opacity = 0.8;
        }
        if (cur_comp_obj.model.ignoreShape()) {
          opacity = (category == layout.ComponentCategory.kVirtual) ? 0 : 0.1;
        }
        shapes.push({
          name: comp_name,
          type: 'path',
          opacity: opacity,
          fillcolor: this._getComponentColorByType(cur_comp_obj.model.type()),
          path: path,
          line: { color: this._getComponentColorByType(cur_comp_obj.model.type()) }
        });
      }
      return shapes;
    },

    doPaintByMicros: async function (selected_micros) {
      if (selected_micros == null) return new Set();
      //var start_micros = Math.min(...selected_micros);
      //var end_micros = Math.max(...selected_micros) + 1;
      var data_raw = await window.grpc.queryRadarPageDatas(selected_micros);
      return await this.doPaint(data_raw);
    },

    todoPaint: async function (data_raw) {
      this.todoPaintDataRaw = data_raw;
      this.todoPaintInterval();
    },

    todoPaintInterval: async function () {
      if (this.todoPaintDataRaw == null) return;
      var tsMillis = new Date().getTime();
      if (tsMillis - this.lastUpdateMillis < 20) return;
      var data = this.todoPaintDataRaw;
      this.lastUpdateMillis = tsMillis;
      this.todoPaintDataRaw = null;
      var selected_microses = await this.doPaint(data);
      DateTimePicker.setTimeByMicrosTs(data.getTimestampMicros());
      var sketchy_items_raw = data.getSketchyItems();
      var sketchy_items_raw = sketchy_items_raw.toObject().itemsList;
      process_sketchy_items(sketchy_items_raw);
      SketchyList.updateItemsRawImpl(sketchy_items_raw, TimestampType.Middle);
      SketchyList.refreshBySelectedMicros(selected_microses);
      SketchyList.adjustSelectItemPosToCenter();
    },

    doPaint: async function (data_raw) {
      this.doPaintContoursImpl(data_raw.getComponentDatas_asU8());
      let motion_items = data_raw.getMotionItems();
      this.doPaintStatusBar(motion_items.toObject().itemsList);
      return await this.doPaintPointCloudAndObstacleImpl(data_raw.getRadarDatasList(), data_raw.getDistanceSensorDatasList_asU8());
    },

    doPaintStatusBar: function (motion_items) {
      var html = `${i18n_data_["currentMotion"]}: `;
      if (motion_items.length == 0) {
        html += i18n_data_["ready"];
      } else {
        for (var i = 0; i < motion_items.length; i += 1) {
          var motion_item = motion_items[i];
          var motion_name = Object.keys(motion.MotionType).find(key => motion.MotionType[key] == motion_item.motionType) || `unknown(id:${motion_item.motionType})`;
          if (i > 0)
            html += ` > `;
          if ((motion_item.rawJson || '').length > 0) {
            html += `<span class="tooltip">${motion_name}<span class="tooltiptext">${motion_item.rawJson}</span></span>`;
          } else {
            html += `<span>${motion_name}</span>`;
          }
        }
      }
      $('#status_bar').html(html);
    },

    doPaintContoursImpl: function (components_raw) {
      var bb = new flatbuffers.ByteBuffer(components_raw);
      components_raw = fuser.ComponentInfoArray.getRootAsComponentInfoArray(bb);
      if (components_raw.dataLength() == 0) return;
      this.rootComponents = null;
      this.components = {};
      for (var i = 0; i < components_raw.dataLength(); i += 1) {
        var model = components_raw.data(i);
        var comp_name = model.uniqueName();
        this.components[comp_name] = new ComponentNode(comp_name, model);
      }
      for (var i = 0; i < components_raw.dataLength(); i += 1) {
        var model = components_raw.data(i);
        var comp_name = model.uniqueName();
        var parent_name = model.baseName();
        if (parent_name == comp_name || this.components[parent_name] == null) {
          this.rootComponents = this.components[comp_name];
        } else {
          this.components[parent_name].children.push(this.components[comp_name]);
        }
      }
      for (var i = 0; i < components_raw.dataLength(); i += 1) {
        var model = components_raw.data(i);
        var comp_name = model.uniqueName();
        RadarPointView._set_abs_position(this.components[comp_name]);
      }
      var shapes = this._getComponentShapes();
      Plotly.relayout(this.viewElem, { shapes: shapes });
    },

    doPaintPointCloudAndObstacleImpl: async function (radar_frames_raw, distance_sensor_raws) {
      var view_sensors = [], view_obsts = [], view_distances = [];
      var selected_microses = new Set();
      var selected_radar_ids = RadarSelector.getCurrentSelectedItems();
      for (i = 0; i < radar_frames_raw.length; i += 1) {
        var radar_frame_raw = radar_frames_raw[i];
        // { forkFramePos: 0, obstacleData: "xx", radarId: 0, scanData: "xx", timestampMicros: 1313 }
        var radar_id = radar_frame_raw.getRadarId();
        var comp_name = RadarSelector.allRadars[radar_id].componentName;
        var comp_obj = this.components[comp_name];
        if (comp_obj != null) {
          selected_microses.add(radar_frame_raw.getTimestampMicros());
          if (!selected_radar_ids.has(radar_id)) continue;

          // paint radar data
          var radar_data = {
            dataCategory: 'sensor',
            name: comp_name,
            x: [],
            y: [],
            type: 'scatter',
            mode: 'markers',
            marker: { color: "#AAAAAA" }
          };
          var bb = new flatbuffers.ByteBuffer(radar_frame_raw.getScanData_asU8());
          var laser_scan = SensorMsg.LaserScan.getRootAsLaserScan(bb);
          var pointCount = laser_scan.rangesLength();
          var angleMin = laser_scan.angleMin();
          var angleIncrement = laser_scan.angleIncrement();
          var abs_pos = comp_obj.model.absPosition();
          for (var j = 0; j < pointCount; j += 1) {
            var angle = angleMin + angleIncrement * j + abs_pos.theta() - Math.PI / 2;
            var val = laser_scan.ranges(j);
            if (val == 0) continue;
            var pt = this._rotatePoint(0, val, angle);
            radar_data.x.push(pt.x + abs_pos.x());
            radar_data.y.push(pt.y + abs_pos.y());
          }
          radar_data.x.push(abs_pos.x());
          radar_data.y.push(abs_pos.y());
          view_sensors.push(radar_data);

          // paint obstacle data
          var buffer = radar_frame_raw.getObstacleData_asU8();
          if (buffer.length != 0) {
            var bb = new flatbuffers.ByteBuffer(buffer);
            var obs = fuser.ObstacleAlarm.getRootAsObstacleAlarm(bb);
            for (var j = 0; j < obs.obstaclesLength(); j += 1) {
              var ob = obs.obstacles(j);
              var arrX = [];
              var arrY = [];
              for (var k = 0; k < ob.posture().verticesLength(); k += 1) {
                var v = ob.posture().vertices(k);
                arrX.push(v.x());
                arrY.push(v.y());
              }
              view_obsts.push({
                dataCategory: 'obstacle',
                name: 'ob-' + ob.id().toFloat64(),
                distanceBeforeCrash: ob.distanceBeforeCrash(),
                distanceBeforeReach: ob.distanceBeforeReach(),
                x: arrX,
                y: arrY,
                type: 'scatter',
                mode: 'lines+markers',
                hovertemplate: '(%{x:.2f}, %{y:.2f})<br>' +
                  'Source: ' + obs.source() + '<br>' +
                  'DistanceBeforeCrash: ' + ob.distanceBeforeCrash().toFixed(2) + '<br>' +
                  'DistanceBeforeReach: ' + ob.distanceBeforeReach().toFixed(2),
                line: {
                  color: this._getObstacleColor(ob.positionType()),
                }
              });
            }
          }
        }
      }
      var view_items = view_sensors.concat(view_obsts);
      //
      for (var i = 0; i < distance_sensor_raws.length; i += 1) {
        var bb = new flatbuffers.ByteBuffer(distance_sensor_raws[i]);
        var distance_sensor_raw = SensorMsg.DistanceSensorStatus.getRootAsDistanceSensorStatus(bb);
        var comp_name = `distance-sensor-${distance_sensor_raw.id()}`;
        var comp_obj = this.components[comp_name];
        if (comp_obj == null) {
          console.log(`unconfigured device: ${comp_name}`);
          continue;
        }
        //
        var point = { x: distance_sensor_raw.distance(), y: 0 };
        var absPos = comp_obj.absPosition;
        var transX = absPos.x + point.x * Math.cos(absPos.theta) - point.y * Math.sin(absPos.theta);
        var transY = absPos.y + point.x * Math.sin(absPos.theta) + point.y * Math.cos(absPos.theta);
        //
        if (comp_obj != null) {
          var distance_data = {
            dataCategory: 'sensor',
            name: comp_name,
            x: [transX],
            y: [transY],
            type: 'scatter',
            mode: 'markers',
            marker: { color: "#AAAAAA" }
          };
        }
        view_distances.push(distance_data);
      }
      view_items = view_items.concat(view_distances);
      //
      Plotly.react(this.viewElem, view_items, this.viewElem.layout);
      return selected_microses;
    }
  };

  var i18n_data_ = {};
  function set_i18n_data(data) {
    i18n_data_ = JSON.parse(data);
    $('#txt_choice').text(`${i18n_data_["pleaseChoice"]}:&emsp;`);
    $('#filtertime_btn').val(i18n_data_["filterTime"]);
    $('#datetimepicker_btn').val(i18n_data_["jump"]);
    $('#txt_timestamp').text(i18n_data_["timestamp"]);
    $('#txt_radar_id').text(i18n_data_["lidarId"]);
    $('#radar_detail_parent_ext span').text(i18n_data_["noData"]);
    $('#status_bar').text(`${i18n_data_["currentMotion"]}: ${i18n_data_["ready"]}`);
    $('#menu_export_2dpoint span').text(i18n_data_["export2dPointFrame"]);
    $('#menu_export_3dpoint span').text(i18n_data_["export3dPointFrame"]);
    $('#menu_view_rgbimage span').text(i18n_data_["viewRgbImage"]);
    $('#menu_view_depthimage span').text(i18n_data_["viewDepthImage"]);
    $('#menu_view_infraimage span').text(i18n_data_["viewInfraImage"]);
    //
    DateTimePicker.init();
    SketchyList.init();
    //
    var init_fn = async () => {
      await RadarSelector.init();
      await RadarPointView.init();
      await SketchyList.updateItems(true, true);
      SketchyList.adjustSelectItemPosToCenter();
    };
    init_fn();
  }

  var DistriDataType = {
    Point2D: 0,
    Point3D: 1,
    ImageRgb24: 2,
    ImageDepth16: 3,
    ImageInfra8: 4,
  };
  var ExporterUtils = {
    CurrentFrameMicros: 0,
    CurrentRadarId: 0,
    CurrentFrameSteadyMillis: 0,

    _downloadFrame: async function (dataType) {
      var tsMicros = this.CurrentFrameMicros;
      var radarId = this.CurrentRadarId;
      var steadyMillis = this.CurrentFrameSteadyMillis;
      return await window.grpc.downloadRadarFrameData(tsMicros, radarId, steadyMillis, dataType);
    },

    _onShow: async function (dataType) {
      if (dataType == DistriDataType.ImageRgb24
        || dataType == DistriDataType.ImageDepth16
        || dataType == DistriDataType.ImageInfra8) {
        var resp = await this._downloadFrame(dataType);
        var filename = resp.getFilename();
        if (filename.endsWith('jpg')) {
          filename = "data:image/jpeg;base64";
        } else {
          filename = "data:image/png;base64";
        }
        console.log(filename);
        var data = `${filename},${resp.getContent_asB64()}`;
        $('#image_view_ctx').attr('src', data);
        this.showImage();
      } else {
        console.log(`[exporter] unsupported show type: ${dataType}`);
      }
    },

    _onExport: async function (dataType) {
      var tsMicros = this.CurrentFrameMicros;
      if (tsMicros == 0) return;
      if (dataType == DistriDataType.Point2D || dataType == DistriDataType.Point3D) {
        // // DistriDataType.Point2D
        // window.grpc.createRadarDataDumpFile(tsMicros, tsMicros + 1, async function (data) {
        //     if (data.getPercent() == 100) {
        //         try {
        //             var info = await window.grpc.getRadarDataDumpFileInfo();
        //             var path = info.getPath();
        //             if (path != null && path != "") {
        //                 var query_params = new URLSearchParams(window.location.search);
        //                 var service_host = query_params.get('serviceHost');
        //                 var a = document.createElement('a');
        //                 a.href = `http://${service_host}:8080/${path}`;
        //                 var p = path.lastIndexOf('.');
        //                 var time_str = new Date().format('yyyyMMdd_HHmmss');
        //                 a.download = `${path.substr(0, p)}.${time_str}.${path.substr(p + 1)}`;
        //                 document.body.appendChild(a);
        //                 a.click();
        //                 document.body.removeChild(a);
        //                 $('#status_bar').text('frame is downloaded. unzip:  gzip -d < radar_dump.gz > radar_dump.bin');
        //             } else {
        //                 $('#status_bar').text('generate failed. internal error.');
        //             }
        //         } catch (e) {
        //             $('#status_bar').text(`generate failed: ${e}`);
        //         }
        //     }
        // });
        try {
          var resp = await this._downloadFrame(dataType);
          var data = resp.getContent_asU8();
          const blob = new Blob([data], { type: 'application/octet-stream' });
          const url = window.URL.createObjectURL(blob);
          const a = document.createElement('a');
          a.href = url;
          a.download = resp.getFilename();
          document.body.appendChild(a);
          a.click();
          document.body.removeChild(a);
          window.URL.revokeObjectURL(url);
        } catch (e) {
          console.log(e);
        }
      } else {
        console.log(`[exporter] unsupported export type: ${dataType}`);
      }
    },

    init: function () {
      $('#shadow_view').click(function () {
        $('#shadow_view').hide();
      });
      $('#menu_export_2dpoint').click(async function () {
        await ExporterUtils._onExport(DistriDataType.Point2D);
      });
      $('#menu_export_3dpoint').click(async function () {
        await ExporterUtils._onExport(DistriDataType.Point3D);
      });
      $('#menu_view_rgbimage').click(async function () {
        await ExporterUtils._onShow(DistriDataType.ImageRgb24);
      });
      $('#menu_view_depthimage').click(async function () {
        await ExporterUtils._onShow(DistriDataType.ImageDepth16);
      });
      $('#menu_view_infraimage').click(async function () {
        await ExporterUtils._onShow(DistriDataType.ImageInfra8);
      });
    },

    onContextMenu: function (e, radarId, frameMicros, steadyMillis, validTypes) {
      e.preventDefault();

      this.CurrentRadarId = radarId;
      this.CurrentFrameMicros = frameMicros;
      this.CurrentFrameSteadyMillis = steadyMillis;
      $('#menu_export_2dpoint').hide();
      $('#menu_export_3dpoint').hide();
      $('#menu_view_rgbimage').hide();
      $('#menu_view_depthimage').hide();
      $('#menu_view_infraimage').hide();
      for (var vt in validTypes) {
        if (vt == DistriDataType.Point2D) {
          $('#menu_export_2dpoint').show();
        } else if (vt == DistriDataType.Point3D) {
          $('#menu_export_3dpoint').show();
        } else if (vt == DistriDataType.ImageRgb24) {
          $('#menu_view_rgbimage').show();
        } else if (vt == DistriDataType.ImageDepth16) {
          $('#menu_view_depthimage').show();
        } else if (vt == DistriDataType.ImageInfra8) {
          $('#menu_view_infraimage').show();
        }
      }
      var doc = $('html').offset();
      this.showMenu(e.pageX - doc.left, e.pageY - doc.top);
    },

    showMenu: function (x, y) {
      $('#popup_menu').css('left', x + 'px');
      $('#popup_menu').css('top', y + 'px');
      $('#popup_menu').show();
      $('#image_view').hide();
      $('#shadow_view').show();
    },

    showImage: function () {
      $('#popup_menu').hide();
      $('#image_view').show();
      $('#shadow_view').show();
    }
  };

  $(function () {
    ExporterUtils.init();
    setInterval(RadarPointView.todoPaintInterval, 20);
  });
</script>
</body>

</html>